React(15) - context vs Jotai


Posted by TempuraEngineer on 2023-03-29

目錄


Jotai是甚麼

Jotai和Redux一樣是用於全域狀態管理的套件

全域狀態管理是指將狀態存在外部的store,因此不會像useState、useContext和組件切不開

故可以用全域性的一次只出現一個的snackbar、dialog上

Jotai有以下幾個特色

  1. bundle size較小

    npm trend

  2. 將一大塊的全域狀態(state tree),切成小塊變成atom

  3. 只有引入某個atom的component會在該atom被更新時re-render降低效能衝擊
  4. 寫法類似useState、Redux,心智負擔較低


Jotai用法

atom

用於存放基本值

interface ClothesDetail {
    color: string;
    material: string;
    type?: string;
    brand?: string;
    price?: number;
}

const clothesDetailAtom = atom<ClothesDetail>({
    color:'black',
    material: 'cutton,
});

使用時

const [clothesDetail, setClothesDetail] = useAtom(clothesDetailAtom);


read-only atom & write-only

read-only atom和write-only atom可以定義成只存取或更新atom的部分屬性

也就是說如果需要的話你甚至可以一個屬性拆出一個atom出來

// read-only
const getClothesDetailAtom = atom((get) =>
  get(clothesAtom),
);

// write-only
const changeClothesDetailAtom = atom(
  null,
  (get, set, update: Partial<ClothesDetail>) => {
    const clothesDetail = get(clothesAtom);

    set(clothesAtom, { ...clothesDetail, ...update });
  }
);

使用時

const [clothesDetail] = useAtom(getClothesDetailAtom);
const [, changeClothesDetail] = useAtom(changeClothesDetailAtom);


select atom

用於存取物件,尤其是物件中包了物件的結構,或者避免陷入infinite loop

接受2個callback,第一個是selector function,第二個是equality function

預設情況下,當參照的atom變動時selector function也會執行,因此可以得到參照的atom的部分current value

如果希望selector function只在reference一樣時執行,可以傳equality function去做比較

而reference一樣是傳址之意,當參照的atom值整個被重新賦值時,就是不一樣的reference

const coordinationAtom = atom({
    top:{
        type:'t-shirt'
        color:'black',
        material:'cotton'
    },
    bottom::{
        type:'gaucho pants'
        color:'white',
        material:'linen'
    }
})

const topCoordinationAtom = selectAtom(coordinationAtom, (coordination) => coordination.top);
const bottomCoordinationAtom = selectAtom(coordinationAtom, ({bottom}) => bottom);

使用時

const [topCoordination] = useAtom(topCoordinationAtom);
const [{color, material, type}] = useAtom(bottomCoordinationAtom);

至於避免infinite loop,是因為select atom可以提供一個穩定的reference,因此就可以避免這個問題


split atom

用於陣列,共有remove、insert、move這幾種類型的事件可以用

const shoppingCartAtom = atom([{
        type:'t-shirt'
        color:'black',
        material:'cotton',
        price:300
    },
    {
        type:'long skirt'
        color:'navy',
        material:'denim',
        price:500
    }
]);

const shoppingCartListAtom = splitAtom(shoppingCartAtom);

使用時

const shoppingCart = () => {
  const [shoppingCartList, dispatch] = useAtom(shoppingCartListAtom);

  return (
    <ul>
      {shoppingCartList.map((item) => (
        <CardItem
          item={item}
          // 類似Redux的dispatch
          remove={() => dispatch({ type: "remove", atom: item })}
        />
      ))}
    </ul>
  );
};


async atom

Jotai支援非同步的讀與寫

傳非同步的callback給atom就能建立一個async atom

用於存取非同步的值(ex:打API撈資料、一些需要使用Promise處理的特殊情境)

// async read-only
const fetchUrlAtom = atom(
  async (get) => {
    const response = await fetch('your API url here')
    return await response.json()
  }
);

使用時

const [apiData] = useAtom(fetchUrlAtom)

(2023/6/13更新)
從v2開始,async atom變成就只是個回傳值為promise的atom,它並不會處理promise
,所以asyn atom的read function需要加上await或者.then()


(2023/6/13更新)

store API

從v2開始移除了JotaiProvider的scope props,並新增了store API的功能

可以把它想像成是Redux的store的概念,store可以傳給jotai Provider

使用createStore建立store

    import { createStore } from 'jotai' // or from 'jotai/vanilla'

    const store = createStore();
    // createStore接收初始值,並回傳store物件
    // 也可以建立React Context並傳給store

store有三個方法,get、set、sub

  • get
    store.get(fooAtom)
    
  • set
    store.set(fooAtom, 1)
    
  • sub
    監聽state的改變
      const unsub = store.sub(fooAtom, () => {
        console.log('fooAtom value in store is changed')
      })
    


getDefaultStore & Provider

jotai Provider組件提供state給component sub tree,且多個Provider是同時獨立存在的

使用Provider有幾個優點

  1. 拆分sub tree的state,避免互相污染
  2. 重新渲染時清空atom

如果某個atom不在Provider下,這就是provider-less mode,這時會使用default的state

default state也可以用getDefaultStore方法取得


情境

假設有3個組件分別如下

└── CreatePhotoWork(上傳作品(照片url、敘述)的頁面)
├── FileUpload(上傳圖片的組件)
└── ImageCrop(切裁圖片的組件)

操作流程則如下

  1. 在FileUpload選擇圖片,觸發input的onChange
  2. 從input取得File
  3. 把File丟給ImageCrop,然後轉成切裁的圖
  4. a. 切裁完按下ImageCrop的OK後,(呼叫setPreview方法)繪製canvas
    b. 將canvas轉成Blob,再轉成File
  5. (這一步在下方例子會省略)
    a. 按下FileUpload的upload,跳出loading
    b. 把從ImageCrop得到的File傳給後端)


使用context實作

context可以讓多個組件共享資料,而不必一層一層地把props傳到最底下

呼叫useContext的組件能夠取得最靠近的Context.Provider的value

然而缺點是,若子組件需要能更新context的值,就必須把set function從父組件傳下去給子組件

如果用以上的情境來說,使用context可以這麼做

  1. 在父組件(頁面組件)建立context,一開始可為空
     import { createContext } from "react";
     export const PhotoStateContext = createContext<PhotoState | null>(null);
    
  2. 在父組件使用useState建立存context值的變數、更新context值的set function

     export type PhotoState = {
       previewSrc: string;
       photoToUpload?: File;
     };
    
     const Context = () => {
       const [photoState, setPhotoState] = useState<PhotoState>({
         previewSrc: "",
         photoToUpload: undefined
       });
    
  3. 在父組件用context建立Context.Provider並把步驟2的變數傳給value
       return (
         <PhotoStateContext.Provider value={photoState}>
            // 省略
        </PhotoStateContext.Provider>
     );
    
  4. a. 在需要能更新context值的子組件開個props,傳入步驟2的set function

     <PhotoStateContext.Provider value={photoState}>
         <FileUpload inputRef={inputRef} setPhotoState={setPhotoState} />
         <ImageCrop
           src={photoState.previewSrc}
           input={inputRef.current}
           setPhotoState={setPhotoState}
         />
     </PhotoStateContext.Provider>
    

    b. 在需要取得context值的子組件使用useContext,並傳入步驟1建立的context

     import { createContext } from "react";
    
     interface ImageCropProps {
        src: string;
        input: HTMLInputElement | null;
        setPhotoState: (data: PhotoState) => void;
     }
    
     const ImageCrop: FunctionComponent<ImageCropProps> = ({
       src,
       input,
       setPhotoState
     }) => {
         const photoState = useContext(PhotoStateContext);
    

codesandbox


使用Jotai實作

如果用以上的情境來說,使用Jotai可以這麼做

  1. 開一個存放atom的檔案(ex: store.ts),並使用atom建立一個存放state的atom

     import { atom } from "jotai";
    
     interface PhotoState {
         photoToUpload?: File;
         previewSrc: string;
     }
    
     export const photoStateAtom = atom<PhotoState>({
         previewSrc: ""
     });
    
  2. 在store.ts視需求撰寫write或read atom

     // 這是write-only atom
     // getter為null,setter為function,所以只能更新
     export const changePhotoStateAtom = atom(
       null,
       (get, set, update: Partial<PhotoState>) => {
         const photoState = get(photoStateAtom);
    
         set(photoStateAtom, { ...photoState, ...update });
       }
     );
    
  3. 在需要取得、更新atom值的地方引入atom

    之後的用法就類似useState

     import { useAtom } from "jotai";
     import { photoStateAtom } from "../../store";
    
     const Jotai = () => {
       // 第一個元素是atom值,第二個是set function
       const [{ previewSrc, photoToUpload }] = useAtom(photoStateAtom);
    
     import { useAtom } from "jotai";
     import { changePhotoStateAtom } from "../../store";
    
     interface ImageCropProps {
       src: string;
       input: HTMLInputElement | null;
     }
    
     const ImageCrop: FunctionComponent<ImageCropProps> = ({ src, input }) => {
       // write-only atom
       // 第一個元素為null,第二個為set function
       const [, setPhotoState] = useAtom(changePhotoStateAtom);
    

codesandbox


參考資料

Jotai
Jōtai 介紹
Jotai - Provider
Jotai - v2 API migration


#React #useContext #jotai #useAtom #createStore







Related Posts

Angular17 基於 Standalone 專案載入 Material Symbols (Google Icon)

Angular17 基於 Standalone 專案載入 Material Symbols (Google Icon)

LeetCode JS 1. Two Sum

LeetCode JS 1. Two Sum

3. ECMAScript - Notational Conventions 符號約定

3. ECMAScript - Notational Conventions 符號約定


Comments